
//Namespace
package com.katujo.squirrel;

//Java imports
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

//Squirrel imports
import com.katujo.squirrel.annotations.Auto;
import com.katujo.squirrel.annotations.Column;
import com.katujo.squirrel.annotations.ColumnPattern;
import com.katujo.squirrel.annotations.ID;
import com.katujo.squirrel.annotations.Transient;
import com.katujo.squirrel.annotations.Link;

/**
 * A Squirrel object holds meta data on a class to be used when interacting
 * with the data base for a object of that class.
 * 
 * <br/><br/>
 * Copyright 2010 Katujo LTD<br/>
 * Licensed under the Apache License, Version 2.0<br/>
 * (the "License"); you may not use this file except in compliance with the License. You may<br/>
 * obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required <br/>
 * by applicable law or agreed to in writing, software distributed under the License is distributed <br/>
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either <br/>
 * express or implied. See the License for the specific language governing permissions and <br/>
 * limitations under the License. <br/>
 * 
 * @author Johan Hertz
 *
 */
public class SquirrelObject
{
	//Fields
	private final Set<Field> fields;
	private final Map<String, Field> fieldsMap;
	private final Set<Field> noneIDFields;
	private final Set<Field> idFields;
	private final Set<Field> linkFields;
	private final Set<Field> linkInsertDeleteFields;
	private final Map<Field, Map<String, Object>> linkMap;
	private final Map<Field, String> linkKeys;
	private final Map<Field, Integer> linkDepth;
	private final Map<Field, String> columns;
	private final Field autoIncrementField;
	private final String insert;
	private final String update;
	private final String delete;
	private final String select;
	private final String selectAll;
	private final Constructor<?> constructor;
	private final static Object[] noArgs = new Object[0];
	
	/**
	 * Create the Object 
	 * @param item
	 */	
	public SquirrelObject(SquirrelManager manager, Class<?> clazz) throws SquirrelException
	{
		//Try to create the squirrel object
		try
		{					
			//Get the fields
			fields = findFields(clazz);
			
			//Build the fields map
			fieldsMap = buildFieldMap(fields);
			
			//Get the none ID fields
			noneIDFields = findNoneIDFields(fields);
			
			//Find the ID fields
			idFields = findIDFields(fields);
			
			//Set the auto increment field
			autoIncrementField = findAutoIncrement(fields);
			
			//Get the link fields
			linkFields = findLinkFields(clazz);
			
			//Get the none select only fields
			linkInsertDeleteFields = findLinkInsertDeleteFields(linkFields);
			
			//Build the link map
			linkMap = buildLinkMap(fields, linkFields);
			
			//Build the link key map
			linkKeys = buildLinkKeys(linkFields);
			
			//Build the link depth map
			linkDepth = builLinkDepth(linkFields);
			
			//Build the columns map
			columns = buildColumns(manager, fields, clazz);
			
			//Create the insert statement 
			insert = SquirrelWriter.writeInsert(manager, clazz, fields, columns, autoIncrementField);
			
			//Create the update statement
			update = SquirrelWriter.writeUpdate(manager, clazz, noneIDFields, idFields, columns);
			
			//Create the delete statement
			delete = SquirrelWriter.writeDelete(manager, clazz, idFields, columns);
			
			//Create the select statement
			select = SquirrelWriter.writeSelect(manager, clazz, fields, idFields, columns);
			
			//Create the select all statement
			selectAll = SquirrelWriter.writeSelectAll(manager, clazz, fields, columns);		
			
			//Get the no arguments constructor
			Class<?>[] args = new Class[0]; 
			constructor = clazz.getConstructor(args);
		}
		
		//Failed
		catch(Exception ex)
		{
			throw new SquirrelException("Failed to create SquirrelObject for " + clazz.getSimpleName(), ex);
		}
	}
	
	/**
	 * Find the fields on a class that Squirrel will try to persist.
	 * @param clazz
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findFields(Class<?> clazz) throws SquirrelException
	{
		return findFields(clazz, null);
	}
	
	/**
	 * Find the fields on a class that Squirrel will try to persist.
	 * @param clazz
	 * @param fields
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findFields(Class<?> clazz, Set<Field> fields) throws SquirrelException
	{
		//Try to find the fields of a object
		try
		{				
			//Get the super class
			Class<?> superClass = clazz.getSuperclass();
			
			//Get the set for the super class if set
			if(superClass != null)
				fields = findFields(superClass, fields);
			
			//Create the fields if not set
			if(fields == null)
				fields = new LinkedHashSet<Field>();
						
			//Get the fields
			Field[] classFields = clazz.getDeclaredFields();
	
			//Loop the fields
			for(Field field : classFields)
			{
				//Get the transient annotation
				boolean isNotTransient = field.getAnnotation(Transient.class) == null;
				
				//Get the link
				boolean isNotLink = field.getAnnotation(Link.class) == null; 
				
				//Get the modifiers
				int modifiers = field.getModifiers();
				
				//Check that the field is private or protected
				if((Modifier.isPrivate(modifiers) || Modifier.isProtected(modifiers)) && 
					!Modifier.isTransient(modifiers) && 
					!Modifier.isStatic(modifiers) &&
					isNotTransient && isNotLink)
				{									
					//Change so field can be accessed (only by reflection)
					field.setAccessible(true);
										
					//Add the field to the set
					fields.add(field);					
				}
			}
			
			//Return the fields
			return fields;
		}
		
		//Failed
		catch(Exception ex)
		{
			throw new SquirrelException("Failed to find fields on class: " + clazz.getName(), ex);
		}
	}
	
	/**
	 * Build the fields map from the fields set
	 * @param fields
	 * @return
	 */
	private static Map<String, Field> buildFieldMap(Set<Field> fields)
	{
		//Create the map
		Map<String, Field> map = new HashMap<String, Field>(); 
		
		//Add the fields to the map
		for(Field field : fields)
			map.put(field.getName(), field);
		
		//Return the map
		return map;
	}
	
	/**
	 * Find the none ID fields
	 * @param fields
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findNoneIDFields(Set<Field> fields) throws SquirrelException
	{
		//Create the set to hold the none ID fields
		Set<Field> noneFieldsID = new LinkedHashSet<Field>();
		
		//Add the none ID fields
		for(Field field : fields)
		{
			//Get the ID annotation
			ID id = field.getAnnotation(ID.class);
			
			//Add if NOT set
			if(id == null)
				noneFieldsID.add(field);
		}
		
		//Return the none ID fields
		return noneFieldsID;
	}
	
	/**
	 * Find the ID fields
	 * @param fields
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findIDFields(Set<Field> fields) throws SquirrelException
	{
		//Create the set to hold the ID fields
		Set<Field> idFields = new LinkedHashSet<Field>();
		
		//Add the ID fields
		for(Field field : fields)
		{
			//Get the ID annotation
			ID id = field.getAnnotation(ID.class);
			
			//Add if set
			if(id != null)
				idFields.add(field);
		}
		
		//Return the ID fields
		return idFields;
	}	
	
	/**
	 * Find the auto increment field
	 * @param fields
	 * @return
	 * @throws SquirrelException
	 */
	private static Field findAutoIncrement(Set<Field> fields) throws SquirrelException
	{		
		//Try to find the auto increment
		for(Field field : fields)
		{			
			//Return if set
			if(field.getAnnotation(Auto.class) != null)
				return field;
		}
		
		//No AutoIncrement
		return null;
	}	
	
	/**
	 * Find the fields marked as @Link on the class. 
	 * @param clazz
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findLinkFields(Class<?> clazz) throws SquirrelException
	{
		return findLinkFields(clazz, null);
	}
	
	/**
	 * Find the fields marked as @Link on the class.
	 * @param clazz
	 * @param fields
	 * @return
	 * @throws SquirrelException
	 */
	private static Set<Field> findLinkFields(Class<?> clazz, Set<Field> fields) throws SquirrelException
	{
		//Try to find the link fields of a class
		try
		{				
			//Get the super class
			Class<?> superClass = clazz.getSuperclass();
			
			//Get the set for the super class if set
			if(superClass != null)
				fields = findLinkFields(superClass, fields);
			
			//Create the fields if not set
			if(fields == null)
				fields = new LinkedHashSet<Field>();
						
			//Get the fields
			Field[] classFields = clazz.getDeclaredFields();
	
			//Loop the fields
			for(Field field : classFields)
			{				
				//Get the link boolean
				boolean isLink = field.getAnnotation(Link.class) != null; 
				
				//Get the modifiers
				int modifiers = field.getModifiers();
				
				//Check to add the field
				if((Modifier.isPrivate(modifiers) || Modifier.isProtected(modifiers)) &&  
					!Modifier.isStatic(modifiers) && isLink)
				{									
					//Change so field can be accessed (only by reflection)
					field.setAccessible(true);
										
					//Add the field to the set
					fields.add(field);					
				}
			}
			
			//Return the fields
			return fields;
		}
		
		//Failed
		catch(Exception ex)
		{
			throw new SquirrelException("Failed to find link fields on class: " + clazz.getName(), ex);
		}
	}
	
	/**
	 * Find all the link fields that are not select only.
	 * @param clazz
	 * @param fields
	 * @return
	 */
	private static Set<Field> findLinkInsertDeleteFields(Set<Field> fields)
	{
		//Create the set
		Set<Field> insertDeleteFields = new LinkedHashSet<Field>();
		
		//Loop the fields
		for(Field field : fields)
		{
			//Get the link annotation
			Link link = field.getAnnotation(Link.class);
			
			//Add if not select only
			if(!link.selectOnly())
				insertDeleteFields.add(field);			
		}
		
		//Return the set
		return insertDeleteFields;		
	}
	
	/**
	 * Build the link map of fields and column value pairs.
	 * <p>
	 * This build the links map used when linking column(s) in the child
	 * to field(s) on the parent. The inner map holds the column name as
	 * its key(String) and the value(Object) is either a constant or a 
	 * filed on the parent. 
	 * </p>
	 * @param fields
	 * @param linkFields
	 * @return
	 */
	private static Map<Field, Map<String, Object>> buildLinkMap(Set<Field> fields, Set<Field> linkFields) 
	{
		//Create the map
		Map<Field, Map<String, Object>> map = new HashMap<Field, Map<String, Object>>();
		
		//Create a field map
		Map<String, Field> filedMap = new HashMap<String, Field>();
		
		//Populate the field map
		for(Field field : fields)
			filedMap.put(field.getName(), field);
		
		//Build the map
		for(Field field : linkFields)
		{
			//Get the annotation
			Link link = field.getAnnotation(Link.class);
			
			//Get the link map
			String[] linkMap = link.map();
			
			//Create the columns map
			Map<String, Object> columns = new HashMap<String, Object>();
			
			//Add to link map to the map
			for(String item : linkMap)
			{
				//Split the item
				String[] split = item.split("=");
								
				//Create the key and value
				String key = split[0];
				Object value = null;
				
				//Value is a field
				if(split[1].charAt(0) != '\'')
					value = filedMap.get(split[1]);
				
				//Value is a constant
				else value = parseConstant(split[1]);
				
				//Add to the columns map
				columns.put(key, value);		
			}

			//Add to map
			map.put(field, columns);								
		}
				
		//Return the map
		return map;
	}
	
	/**
	 * Find the key names for the link fields that are Maps.
	 * @param fields
	 * @return
	 */
	private static Map<Field, String> buildLinkKeys(Set<Field> fields)
	{
		//Create the map
		Map<Field, String> map = new HashMap<Field, String>();
		
		//Loop the fields and add to the map
		for(Field field : fields)
		{
			//Get the annotation
			Link link = field.getAnnotation(Link.class);	
			
			//Add to the map
			map.put(field, link.key());			
		}
				
		//Return the map
		return map;
	}
	
	/**
	 * Build the link depth map.
	 * @param fields
	 * @return
	 */
	private static Map<Field, Integer> builLinkDepth(Set<Field> fields)
	{
		//Create the map
		Map<Field, Integer> map = new HashMap<Field, Integer>();
		
		//Loop the fields and add to the map
		for(Field field : fields)
		{
			//Get the annotation
			Link link = field.getAnnotation(Link.class);	
			
			//Add to the map
			map.put(field, link.depth());			
		}
				
		//Return the map
		return map;
	}	
	
	/**
	 * Parse a constant to double, int or string.
	 * @param value
	 * @return
	 */
	private static Object parseConstant(String value)
	{
		//Remove ' form start and end
		value = value.substring(1, value.length()-1);
		
		//Create the flags
		boolean dotFound = false;

		//Check type
		for(char c : value.toCharArray())
		{
			//Check if there is one .
			if(c == '.' && !dotFound)
				dotFound = true;
			
			//If char is digit just go on
			else if(Character.isDigit(c));
			
			//This is a string
			else return value;
		}
		
		//Parse as double
		if(dotFound)
			return Double.parseDouble(value);
		
		//Parse as int
		else return Integer.parseInt(value);
	}
	
	/**
	 * Build the columns map.
	 * @param manager
	 * @param fields
	 * @param clazz
	 * @return
	 * @throws SquirrelException
	 */
	private static Map<Field, String> buildColumns(SquirrelManager manager, Set<Field> fields, Class<?> clazz) throws SquirrelException
	{
		//Get the column pattern
		ColumnPattern columnPattern = clazz.getAnnotation(ColumnPattern.class);		
		
		//Get the column pattern from the manager if null
		if(columnPattern == null)
			columnPattern = manager.getClass().getAnnotation(ColumnPattern.class);
		
		//Create the map
		Map<Field, String> map = new HashMap<Field, String>(fields.size());
		
		//Add the fields
		for(Field field : fields)
			map.put(field, getColumnName(field, columnPattern));

		//Return the map
		return map;
	}
		
	/**
	 * Get the column name for a field.
	 * @param field
	 * @param pattern
	 * @return
	 */
	private static String getColumnName(Field field, ColumnPattern pattern)
	{
		//Get the column annotation
		Column column = field.getAnnotation(Column.class);
		
		//User column if set
		if(column != null)
			return column.name();		
		
		//Use name if pattern not set
		if(pattern == null)
			return field.getName();
		
		//Get the column name using the column pattern
		switch(pattern.pattern())
		{
			//Format column name
			case JAVA: return field.getName();
			case JAVA_FIRST_UPPER: return SquirrelWriter.javaUpperPattern(field.getName());
			case UPPER_CASE: return SquirrelWriter.upperPattern(field.getName());
			case LOWER_CASE: return SquirrelWriter.lowerPattern(field.getName());
		
			//If the code gets to the line the code need to be updated.
			default:
				return null;		
		}
	}		
	
	/**
	 * Create a empty object of the underlying type.
	 * @return
	 * @throws SquirrelException
	 */	
	public Object createObject() throws SquirrelException
	{
		//Try to create object
		try		
		{	
			Object item = constructor.newInstance(noArgs);
			return item;
		}
		
		//Failed
		catch(Exception ex)
		{
			throw new SquirrelException("Failed to create object: " + constructor.getName(), ex);
		}
	}	

	/* Properties */

	/** 
	 * Get the insert
	 * @return the insert
	 */
	public String getInsert()
	{
		return insert;
	}

	/** 
	 * Get the update
	 * @return the update
	 */
	public String getUpdate()
	{
		return update;
	}

	/** 
	 * Get the delete
	 * @return the delete
	 */
	public String getDelete()
	{
		return delete;
	}

	/** 
	 * Get the select
	 * @return the select
	 */
	public String getSelect()
	{
		return select;
	}

	/** 
	 * Get the selectAll
	 * @return the selectAll
	 */
	public String getSelectAll()
	{
		return selectAll;
	}

	/** 
	 * Get the fields
	 * @return the fields
	 */
	public Set<Field> getFields()
	{
		return fields;
	}

	/** 
	 * Get the noneIDFields
	 * @return the noneIDFields
	 */
	public Set<Field> getNoneIDFields()
	{
		return noneIDFields;
	}

	/** 
	 * Get the idFields
	 * @return the idFields
	 */
	public Set<Field> getIdFields()
	{
		return idFields;
	}

	/** 
	 * Get the autoIncrementField
	 * @return the autoIncrementField
	 */
	public Field getAutoIncrementField()
	{
		return autoIncrementField;
	}		
	
	/** 
	 * Get the columns
	 * @return the columns
	 */
	public Map<Field, String> getColumns()
	{
		return columns;
	}	
	
	/** 
	 * Get the linkMap
	 * @return the linkMap
	 */
	public Map<Field, Map<String, Object>> getLinkMap()
	{
		return linkMap;
	}

	/** 
	 * Get the linkFields
	 * @return the linkFields
	 */
	public Set<Field> getLinkFields()
	{
		return linkFields;
	}

	/** 
	 * Get the linkInsertDeleteFields
	 * @return the linkInsertDeleteFields
	 */
	public Set<Field> getLinkInsertDeleteFields()
	{
		return linkInsertDeleteFields;
	}

	/** 
	 * Get the fieldsMap
	 * @return the fieldsMap
	 */
	public Map<String, Field> getFieldsMap()
	{
		return fieldsMap;
	}

	/** 
	 * Get the linkKeys
	 * @return the linkKeys
	 */
	public Map<Field, String> getLinkKeys()
	{
		return linkKeys;
	}

	/** 
	 * Get the linkDepth
	 * @return the linkDepth
	 */
	public Map<Field, Integer> getLinkDepth()
	{
		return linkDepth;
	}
		
}
